/*
 * Copyright 2009 Prime Technology.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.primefaces.facestrace.component;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.component.ValueHolder;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;

import org.primefaces.facestrace.TraceData;
import org.primefaces.facestrace.logging.TraceLogEvent;
import org.primefaces.facestrace.phase.PhaseData;
import org.primefaces.facestrace.utils.TraceUtils;

public class TraceRenderer extends Renderer{
	
	private final static String toggleScript = "FacesTraceUtils.toggleSection(this.parentNode);";

	public void encodeEnd(FacesContext facesContext, UIComponent component) throws IOException { 
		if(TraceUtils.isTraceDisabled(facesContext))
			return;
			
		Trace trace = (Trace) component;
		TraceData traceData = getTraceData();
		
		ResponseWriter writer = facesContext.getResponseWriter();

		renderResourceIncludes(facesContext);
		
		writer.startElement("div", trace);
		writer.writeAttribute("class", "ftContainer", null);
		
		writer.startElement("h1", trace);
		writer.write("FacesTrace Log");
		writer.endElement("h1");
		
		renderLifecycle(writer, traceData);
		renderScopeVariables(facesContext);
		renderFacesMessages(facesContext);
		renderLogs(writer, traceData);
		renderComponentTree(facesContext);
		
		writer.endElement("div");
	}

	private void renderLifecycle(ResponseWriter writer, TraceData traceData) throws IOException {
		renderSectionBegin(writer, "Request Lifecycle");
		
		PhaseData[] phases = traceData.getPhases();
		for(int i = 0; i < phases.length; i++) {
			PhaseData phase = phases[i];
			writer.startElement("div", null);
			
			if(i == 0)
				writer.writeAttribute("class", "ftSuccessfullPhase", null);
			else if(i == 4) {
				if(phase.isExecuted())
					writer.writeAttribute("class", "ftSuccessfullPhase", null);
				else
					writer.writeAttribute("class", "ftSkippedPhase", null);
			}
			else {
				if(phase.isCausedResponse())
					writer.writeAttribute("class", "ftFailedPhase", null);
				else if(phase.isExecuted())
					writer.writeAttribute("class", "ftSuccessfullPhase", null);
				else
					writer.writeAttribute("class", "ftSkippedPhase", null);
			}
			
			
			writer.write(phase.getPhaseName());
			writer.write("<br />");
			writer.write(phase.getExecutionTime() + "ms");
			writer.endElement("div");
			writer.write("\n");
		}
		
		renderSectionEnd(writer);
	}
	
	private void renderScopeVariables(FacesContext facesContext) throws IOException{
		ResponseWriter writer = facesContext.getResponseWriter();
		renderSectionBegin(writer, "Variables");
		
		writer.startElement("table", null);
		
		renderTableHeaders(writer, new String[]{"Variable", "Key", "Value"});
		
		writer.startElement("tbody", null);
		
		String[] variables = new String[]{"param","requestScope","sessionScope","header","initParam","applicationScope","cookie"};
		for (int i = 0; i < variables.length; i++) {
			Map variableMap = getVariableMap(facesContext, variables[i]);
			Set keySet = variableMap.keySet();
			int keyIndex = 0;
			
			for (Iterator iterator = keySet.iterator(); iterator.hasNext();) {
				String key = (String) iterator.next();
				writer.startElement("tr", null);
				if(i%2==0)
					writer.writeAttribute("class", "even", null);
				else
					writer.writeAttribute("class", "odd", null);
				
				if(keyIndex == 0) {
					writer.startElement("td", null);
					writer.writeAttribute("rowspan", String.valueOf(keySet.size()), null);
					writer.write(variables[i]);
					writer.endElement("td");
				}
				
				writer.startElement("td", null);
				writer.write(key);
				writer.endElement("td");
				
				writer.startElement("td", null);
				Object value = variableMap.get(key);
				if(value != null) {
					String valueAsText = escapeHtml(value.toString());
					if(valueAsText.length() > 30)
						writer.write(valueAsText.substring(0,30) + "...");
					else
						writer.write(valueAsText);
				}
				else {
					writer.write("NULL");
				}
				
				writer.endElement("td");
				
				
				writer.endElement("tr");
				keyIndex = keyIndex + 1;
			}
		}
		
		writer.endElement("tbody");
		
		writer.endElement("table");
		
		renderSectionEnd(writer);
	}
	
	private String escapeHtml(String value) {
		return value.replaceAll("<", "&lt;").replaceAll(">","&gt;");
	}
	
	private void renderFacesMessages(FacesContext facesContext) throws IOException{
		ResponseWriter writer = facesContext.getResponseWriter();
		
		renderSectionBegin(writer, "Faces Messages");
		
		writer.startElement("table", null);
		renderTableHeaders(writer, new String[]{"Severity", "Summary", "Detail"});
		writer.startElement("tbody", null);
		
		Iterator iter = facesContext.getMessages();
		if(!iter.hasNext()) {
			writer.startElement("tr", null);
			writer.startElement("td", null);
			writer.writeAttribute("colspan", "3", null);
			writer.write("There are no faces messages");
			writer.endElement("td");
			writer.endElement("tr");
		}
		
		boolean even = true;
		while(iter.hasNext()) {
			FacesMessage msg = (FacesMessage) iter.next();
			writer.startElement("tr", null);
			
			if(even)
				writer.writeAttribute("class", "even", null);
			else
				writer.writeAttribute("class", "odd", null);
			
			writer.startElement("td", null);
			writer.write(msg.getSeverity().toString());
			writer.endElement("td");
			
			writer.startElement("td", null);
			writer.write(msg.getSummary().toString());
			writer.endElement("td");
			
			writer.startElement("td", null);
			writer.write(msg.getDetail().toString());
			writer.endElement("td");
			
			even = !even;
			
			writer.endElement("tr");
		}
		
		writer.endElement("tbody");
		writer.endElement("table");
		renderSectionEnd(writer);
	}
	
	private void renderLogs(ResponseWriter writer, TraceData traceData) throws IOException{
		renderSectionBegin(writer, "Logs");
		
		writer.startElement("table", null);
		renderTableHeaders(writer, new String[]{"Logger", "Level", "Message"});
		writer.startElement("tbody", null);
		
		List logs = traceData.getLogs();
		if(logs.size() == 0) {
			writer.startElement("tr", null);
			writer.startElement("td", null);
			writer.writeAttribute("colspan", "3", null);
			writer.write("There are no logs appended");
			writer.endElement("td");
			writer.endElement("tr");
		}
		
		for (int i = 0; i < logs.size(); i++) {
			TraceLogEvent logEvent = (TraceLogEvent) logs.get(i);
			
			writer.startElement("tr", null);
			
			if(i%2 == 0)
				writer.writeAttribute("class", "even", null);
			else
				writer.writeAttribute("class", "odd", null);
			
			writer.startElement("td", null);
			writer.write(logEvent.getName());
			writer.endElement("td");
			
			writer.startElement("td", null);
			writer.write(logEvent.getType());
			writer.endElement("td");
			
			writer.startElement("td", null);
			writer.write(logEvent.getMessage());
			writer.endElement("td");
			
			writer.endElement("tr");
		}
		
		writer.endElement("tbody");
		writer.endElement("table");
		renderSectionEnd(writer);
	}
	
	private void renderComponentTree(FacesContext facesContext) throws IOException{
		ResponseWriter writer = facesContext.getResponseWriter();
		renderSectionBegin(writer, "Component Tree");
		
		UIViewRoot root = facesContext.getViewRoot();
		if (root == null)
			return;
		
		writer.write("\n<ul>\n");
		renderComponent(root, writer);
		writer.write("</ul>\n");
		
		renderSectionEnd(writer);
	}
	
	private void renderComponent(UIComponent component, ResponseWriter writer) throws IOException {
		renderComponentInfo(component, writer);
		if (component.getChildCount() > 0)
			renderChildrenInfo(component.getChildren(), writer);
	}
	
	private void renderComponentInfo(UIComponent component, ResponseWriter writer) throws IOException {
		String id = component.getId();
		String componentType = component.getClass().getName();
		writer.write("<li");
		if (component.getChildCount() > 0)
			writer.write(" id=\"foldheader\"");

		writer.write(">id:\"" + id + "\" | type:\"" + componentType + "\"");
		if (component instanceof ValueHolder) {
			if (component.isRendered()) {
				Object value = ((ValueHolder) component).getValue();
				if(value != null) {
					String valueAsText = value.toString();	//TODO: use a converter to get the string value
					
					if(valueAsText.length() > 30)
						writer.write(" | value:\"" + escapeHtml(valueAsText.substring(0,30)) + "..." + "\"</li>\n");
					else
						writer.write(" | value:\"" + escapeHtml(valueAsText) + "\"</li>\n" );
				} else {
					writer.write(" | value: null</li>\n");
				}
			} else {
				writer.write(" | rendered:\"false\"</li>\n");
			}
		} else {
			writer.write("</li>\n");
		}
	}

	private void renderChildrenInfo(List list, ResponseWriter writer) throws IOException {
		writer.write("<ul id=\"foldinglist\" style=\"display:none\">\n");
		for (Iterator iter = list.iterator(); iter.hasNext();) {
			UIComponent child = (UIComponent) iter.next();
			renderComponent(child, writer);
		}
		writer.write("</ul>\n");
	}
	
	private void renderSectionBegin(ResponseWriter writer, String header) throws IOException{
		writer.startElement("div", null);
		writer.writeAttribute("class", "ftSection", null);
		
		writer.startElement("h3", null);
		writer.writeAttribute("onclick", toggleScript, null);
		writer.write(header);
		writer.endElement("h3");
		
		writer.startElement("div", null);
		writer.writeAttribute("class", "ftSectionContent", null);
	}
	
	private void renderSectionEnd(ResponseWriter writer) throws IOException{
		writer.endElement("div");
		writer.endElement("div");
	}
	
	private void renderTableHeaders(ResponseWriter writer, String[] headers) throws IOException{
		writer.startElement("thead", null);
		
		for (int i = 0; i < headers.length; i++) {
			writer.startElement("th", null);
			writer.write(headers[i]);
			writer.endElement("th");
		}
		
		writer.endElement("thead");
	}
	
	private void renderResourceIncludes(FacesContext facesContext) throws IOException {
		renderScriptInclude(facesContext);
		renderFacesStyleInclude(facesContext);
	}
	
	private String getActionURL(FacesContext facesContext) {
		String viewId = facesContext.getViewRoot().getViewId();
		
		return facesContext.getApplication().getViewHandler().getActionURL(facesContext, viewId);
	}
	
	private String getResourceURL(FacesContext facesContext, String resource) {
		String actionURL = facesContext.getExternalContext().encodeResourceURL(getActionURL(facesContext));
		
		if(actionURL.contains("?"))
			actionURL = actionURL + "&facestraceResource=" + resource;
		else
			actionURL = actionURL + "?facestraceResource=" + resource;
		
		return actionURL;
	}
	
	private void renderScriptInclude(FacesContext facesContext) throws IOException {
		ResponseWriter writer  = facesContext.getResponseWriter();
		
		String resourceURL = getResourceURL(facesContext, "facestrace.js");
		
		writer.startElement("script", null);
		writer.writeAttribute("type", "text/javascript", null);
		writer.writeAttribute("src", resourceURL, null);
		writer.endElement("script");
	}

	private void renderFacesStyleInclude(FacesContext facesContext) throws IOException {
		ResponseWriter writer  = facesContext.getResponseWriter();
		
		String resourceURL = getResourceURL(facesContext, "facesTrace.css");;
		
		writer.startElement("link", null);
		writer.writeAttribute("rel", "stylesheet", null);
		writer.writeAttribute("type", "text/css", null);
		writer.writeAttribute("href", resourceURL, null);
		writer.endElement("link");
	}
	
	private TraceData getTraceData() {
		FacesContext facesContext = FacesContext.getCurrentInstance();
		
		return (TraceData) facesContext.getApplication().createValueBinding("#{facesTraceData}").getValue(facesContext);
	}
	
	private Map getVariableMap(FacesContext facesContext, String variable) {
		return (Map)facesContext.getApplication().createValueBinding("#{" + variable + "}").getValue(facesContext);
	}
	
	
}
